﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Dynamic;
using System.Linq.Expressions;
using System.CodeDom;

using System.Reflection;

using Alicanto.Helpers;

namespace Alicanto.Execution
{
    internal class ExecutionMeta : DynamicMetaObject
    {
        internal delegate Expression PredefinedInvokeMemberHandler(ExecutionContext context, InvokeMemberBinder binder, DynamicMetaObject[] args);
        private static Dictionary<string, PredefinedInvokeMemberHandler> _predefinedInvokeMembers = new Dictionary<string, PredefinedInvokeMemberHandler>()
        {
            {"construct", _handleConstruct}
        };

        internal ExecutionMeta(Expression expression, object value) : base(expression, BindingRestrictions.Empty, value)
        {

        }

        public override DynamicMetaObject BindSetMember(SetMemberBinder binder, DynamicMetaObject value)
        {
            Expression constant = Expression.Empty();
            ExecutionContext context = (ExecutionContext)this.Value;

            // Resolve member
            ExecutionContextMember member = context[binder.Name];
            constant = Expression.Constant(member);

            Expression binding = Expression.Assign(
                Expression.Property(
                    constant,
                    "Member"
                ),
                Expression.Convert(
                    Expression.Constant(value.Value),
                    typeof(object)
                )
            );

            return _wrapRestrictions(binding);
        }

        public override DynamicMetaObject BindGetMember(GetMemberBinder binder)
        {
            Expression constant = Expression.Empty();
            ExecutionContext context = (ExecutionContext)this.Value;

            // Resolve member
            ExecutionContextMember member = context[binder.Name];
            constant = Expression.Constant(member.Member);

            Expression binding = Expression.Convert(
                constant,
                binder.ReturnType
            );

            return _wrapRestrictions(binding);
        }

        public override DynamicMetaObject BindInvokeMember(InvokeMemberBinder binder, DynamicMetaObject[] args)
        {
            ExecutionContext context = (ExecutionContext)this.Value;

            // Look for predefined handlers
            if (_predefinedInvokeMembers.ContainsKey(binder.Name))
                return _wrapRestrictions(_predefinedInvokeMembers[binder.Name](context, binder, args));

            Expression binding = Expression.Convert(
                _invokeMember(context, binder.Name, args),
                binder.ReturnType
            );

            return _wrapRestrictions(binding);
        }

        private DynamicMetaObject _wrapRestrictions(Expression binding)
        {
            return new DynamicMetaObject(
                binding,
                this.Restrictions.Merge(
                    BindingRestrictions.GetInstanceRestriction(this.Expression, this.Value)
                )
            );
        }

        private static Expression _invokeMember(ExecutionContext context, string name, DynamicMetaObject[] args)
        {
            ExecutionContextMember member = context[name];

            if (member == null)
                throw new Exception("Member not found");

            if (!(member.Member is LambdaExpression))
                throw new Exception("Not a function");

            LambdaExpression lambda = (LambdaExpression)member.Member;

            if (lambda.Parameters.Count != args.Length)
                throw new Exception("Argument count does not match");

            // Collect arguments
            IList<Expression> arguments = new List<Expression>();
            for (int i = 0; i < args.Length; i++)
            {
                Expression arg = args[i].Expression;
                arg = Boxing.EnsureBox(arg, lambda.Parameters[i].Type);
                arguments.Add(arg);
            }

            Expression binding = Expression.Invoke(
                lambda,
                arguments
            );

            return _wrapClosure(context, member, binding);
        }

        private static Expression _wrapClosure(ExecutionContext context, ExecutionContextMember member, Expression binding)
        {
            if (member.Scope != null && member.Scope.RequiredVariables.Count > 0)
            {
                List<Expression> closure = new List<Expression>();
                foreach (ParameterExpression variable in member.Scope.RequiredVariables)
                {
                    int index = context.Scope.Declarations.IndexOf(variable);
                    if (index == -1)
                        throw new Exception();

                    if (context.RuntimeVariables.Count == 0 || index >= context.RuntimeVariables.Count)
                        throw new Exception();

                    closure.Add(Expression.Assign(
                        variable,
                        Expression.Constant(context.RuntimeVariables[index])
                    ));
                }

                closure.Add(binding);

                binding = Expression.Block(
                    member.Scope.RequiredVariables,
                    closure
                );
            }

            return binding;
        }

        private static Expression _handleConstruct(ExecutionContext context, InvokeMemberBinder binder, DynamicMetaObject[] args)
        {
            context = context.construct();
            Expression binding = Expression.Block(
                Expression.Convert(
                    _invokeMember(context, "constructor", args),
                    binder.ReturnType
                ),
                Expression.Constant(context)
            );

            return binding;
        }
    }
}
